/*
 * 2012 June
 */
package queue_model;

/**
 *
 * @author Alvaro Echeverría
 *
 *
 */
import Jama.Matrix;
import com.csvreader.CsvReader;
import java.io.IOException;
import java.io.StringReader;
import java.util.Properties;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;

/**
 * Esta clase esta diseñada para definir la cantidad de cajas necesarias para
 * mantener un nivel de servicio de que el 95% de las veces hayan 3 o menos
 * personas por cola. Esto se realiza a traves de un proceso de Markov con
 * estados finitos y tiempo continuo.
 *
 * @author Alvaro Echeverría - Scopix Solutions-
 */
public class Queue_Model {

    /**
     * Clase principal del proyecto.
     */
    private String xml; // Es un String-Xml con la informacion actual del supermercados.
    private int maxcheckout; //Máximo de cajas disponibles en el supermercado (suma de normales y express)
    private String idstore; //ID interno de identificación para el supermercado.

    /**
     * Constructor de la clase Queue_Model.
     *
     * @param s String XMl con datos históricos del supermercado.
     * @param m es el máximo número de cajas que posee el supermercado
     * @param store ID interno de identificación para el supermercado
     */
    public Queue_Model(String s, int m, String store) {
        xml = s;
        maxcheckout = m;
        idstore = store;
    }

    /**
     * Método main de la clase, sólo existe para fines de testeo.
     *
     * @param args the command line arguments
     * @throws Exception when fail to read data from properties files.
     */
    public static void main(final String[] args) throws Exception {
        String xml = "<Proc store_id=\"1953\" hour=\"22:00\" day=\"4\">"
                + "<times>"
                + "<time time=\"22:00\" delta=\"-5\">"
                + "<variable name=\"PC\" value=\"68\"/>"
                + "</time><time time=\"21:45\" delta=\"-20\">"
                + "<variable name=\"PC\" value=\"62\"/>"
                + "<variable name=\"QL\" value=\"4\"/>"
                + "<variable name=\"OC\" value=\"2\"/></time>"
                + "<time time=\"21:30\" delta=\"-35\">"
                + "<variable name=\"PC\" value=\"60\"/>"
                + "<variable name=\"QL\" value=\"1\"/>"
                + "<variable name=\"OC\" value=\"2\"/></time>"
                + "<time time=\"21:15\" delta=\"-50\">"
                + "<variable name=\"PC\" value=\"64\"/>"
                + "<variable name=\"QL\" value=\"0\"/>"
                + "<variable name=\"OC\" value=\"2\"/></time>"
                + "<time time=\"21:00\" delta=\"-65\">"
                + "<variable name=\"PC\" value=\"76\"/>"
                + "<variable name=\"QL\" value=\"3\"/>"
                + "<variable name=\"OC\" value=\"3\"/></time>"
                + "</times>"
                + "<Q15>"
                + "<prediction time=\"21:50\" value=\"5\"/>"
                + "<prediction time=\"21:47\" value=\"5\"/>"
                + "<prediction time=\"21:44\" value=\"4\"/>"
                + "</Q15>"
                + "</Proc>";
        String xml2 = "<Proc day=\"3\" store_id=\"1953\" >"
                + "<time  time=\"08:30\" delta=\"43\">"
                + "<variable  name=\"PC\"  value=\"41.0\" /></time>"
                + "<time  time=\"08:15\" delta=\"28\">"
                + "<variable  name=\"PC\"  value=\"35.0\" /></time>"
                + "<time  time=\"08:00\" delta=\"13\">"
                + "<variable  name=\"PC\"  value=\"37.0\" /></time>"
                + "<time  time=\"07:45\" delta=\"-1\">"
                + "<variable  name=\"PC\"  value=\"37.0\" /></time>"
                + "<time  time=\"07:15\" delta=\"-31\">"
                + "<variable  name=\"PC\"  value=\"20.0\" /></time>"
                + "<time  time=\"07:00\" delta=\"-46\">"
                + "<variable  name=\"PC\"  value=\"20.0\" /></time>"
                + "<time  time=\"06:45\" delta=\"-61\">"
                + "<variable  name=\"PC\"  value=\"29.0\" /></time>"
                + "<time  time=\"06:30\" delta=\"-76\">"
                + "<variable  name=\"PC\"  value=\"25.0\" /></time></Proc>";
        final int cajas = 10;
        final int tamano = 100;
        final String store = "1953";
        final String store2 = "Arboretum";
        int periodo = 1;
        double sl = 0.85;
        Queue_Model qm = new Queue_Model(xml, cajas, store);
        System.out.println(qm.SolveModel(sl));
        //qm.Read_solveModel(periodo, tamano, store);

    }

    /**
     *
     * @param sl es el nivel de servicio requerido al momento de resolver el
     * modelo.
     * @return Devuelve un String XML con la prediccion para los próximos 15,30
     * y 45 minutos. Además de la predicción de la llegada de personas al
     * sistema de cajas, el largo de cola y tiempo de espera.
     * @throws Exception Relacionado a errores en la lectura del XML o si es que
     * no es posible leer alguno de los archivos de configuración.
     */
    public String SolveModel(double sl) throws Exception {
        Properties propiedades = new Properties();
        propiedades.load(this.getClass().getClassLoader().getResourceAsStream("datos.properties"));
        int time = 1;
        int size = 100;
        String store = this.idstore;
        double ServiceLevel = Double.parseDouble(propiedades.getProperty("Service_level"));
        double sla = Double.parseDouble(propiedades.getProperty("people_per_queue"));
        double hora1 = Double.parseDouble(propiedades.getProperty("hora1"));
        double hora2 = Double.parseDouble(propiedades.getProperty("hora2"));
        System.out.println(hora1);
        System.out.println(hora2);
        double lambda;
        double mu;
        double[] ql = XML2Data("QL");
        double[] pc = XML2Data("PC");
        double[] dh = XML2Data("DH");// dh[0] is hour and dh[1] is day number
        double[] result = new double[15];
        //initialize in 1.0 to evaluate minimun number of queues.
        double openqueue = 1.0;
        //double openqueue = XML2Data("OC")[0];
        double criterion = openqueue * sla;
        double sum = 0.0;
        double expected_queue = 0.0;
        Matrix A;
        Matrix B;
        lambda = lambda_properties(store, pc, (int) dh[1], (int) dh[0]);
        mu = mu_properties((int) dh[0], store);
        for (int count = 0; count <= result.length - 1; count++) {
            if (ql.length != 0) {
                while (sum < ServiceLevel) {
                    A = RateTransition(size, (int) dh[0], store, openqueue, pc, dh);
                    B = CTMCTransiente(A, 1);
                    sum = 0.0;
                    for (int i = 0; i < B.getRowDimension(); i++) {
                        for (int j = 0; j < B.getColumnDimension(); j++) {
                            if (ql[0] == i && j <= criterion) {
                                sum += B.get(i, j);
                                expected_queue += j * B.get(i, j);

                            }
                        }

                    }
                    if (sum < ServiceLevel) {
                        openqueue = openqueue + 1.0;
                        criterion = openqueue * sla;
                    }
                }
                double[] pastprediction = XML2Data("Q15");
                if (pastprediction.length != 0 && pastprediction.length >= 2) {
                    if (Math.abs(pastprediction[0] - pastprediction[1]) == 1) {
                    }

                }
                //si la hora es menor que 9, no dar recomendacion.
                if (dh[0] < hora1) {
                    openqueue=1.0;
                    expected_queue=0.0;
                }
                result[count] = openqueue;
                result[count + 1] = (expected_queue / openqueue) + 1;
                result[count + 2] = Math.round((expected_queue / openqueue) * ((1 / mu) * 60 * 15));
                result[count + 3] = 1;
                result[count + 4] = Math.round(lambda);

            } else {
                result[count] = historical(store, (int) dh[0], (int) dh[1])[0];
                result[count + 1] = (historical(store, (int) dh[0], (int) dh[1])[1] / result[count]) + 1;
                result[count + 2] = Math.round((result[count + 1] - 1) * ((1 / mu) * 60 * 15) / result[count]);
                result[count + 3] = 0;
                result[count + 4] = Math.round(lambda);
            }
            count = count + 4;
        }
        return XmlOutputResult(result);

    }

    /**
     * Función solo con propósitos de debuggeo. Esta lee un archivo csv con
     * datos de cantidad de cajeros abiertos, llegadas al supermercado en los
     * intervalos 15, 30,45,60 y 75 minutos atrás.
     *
     * @param time es la cantidad de períodos iteraciones que realizará la
     * matriz de tasas de transición
     * @param size es el tamaño máximo definido para la cola total del
     * supermercado. (suma total de las colas de cada caja).
     * @param store es el id de la tienda donde se esta ejecutando el proceso
     * actual.
     * @throws Exception Excepciones si es que no puede leer alguno de los
     * archivos de parámetros.
     */
    public void Read_solveModel(int time, int size, String store) throws Exception {
        CsvReader csv = new CsvReader("C:/Users/Alvaro/Dropbox/Scopix/SVNSimulacion/Queue_Model/ejemplo.csv");
        csv.readRecord();
        double ServiceLevel = 0.98;
        double sla = 3;

        while (csv.readRecord()) {
            double id = Double.parseDouble(csv.get(0));
            double ql = Double.parseDouble(csv.get(2));
            double[] pc = new double[csv.getColumnCount() - 6];
            double[] dh = new double[2];
            pc[0] = Double.parseDouble(csv.get(4));
            pc[1] = Double.parseDouble(csv.get(5));
            pc[2] = Double.parseDouble(csv.get(6));
            pc[3] = Double.parseDouble(csv.get(7));
            pc[4] = Double.parseDouble(csv.get(8));
            pc[5] = Double.parseDouble(csv.get(9));
            pc[6] = Double.parseDouble(csv.get(10));
            dh[0] = Double.parseDouble(csv.get(11));
            dh[1] = Double.parseDouble(csv.get(12));
            double openqueue = 1.0; //initialize in 1.0 but also could be from XML2Data
            //double openqueue = XML2Data("OC")[0];
            double criterion = openqueue * sla;
            double sum = 0.0;
            double expected_queue = 0.0;
            double lambda = 0.0;
            double mu = 0.0;
            Matrix A;
            Matrix B;
            lambda = lambda_properties(store, pc, (int) dh[1], (int) dh[0]);
            mu = mu_properties((int) dh[0], store);
            while (sum < ServiceLevel) {
                A = RateTransition(size, (int) dh[0], store, openqueue, pc, dh);
                B = CTMCTransiente(A, 1);
                sum = 0.0;
                for (int i = 0; i < B.getRowDimension(); i++) {
                    for (int j = 0; j < B.getColumnDimension(); j++) {
                        if (ql == i && j <= criterion) {
                            sum += B.get(i, j);
                            expected_queue += j * B.get(i, j);
                        }
                    }

                }

                if (sum < ServiceLevel) {
                    openqueue = openqueue + 1.0;
                    criterion = openqueue * sla;
                }


            }
            System.out.println(id + " " + openqueue + " " + ((expected_queue / openqueue) + 1) + " " + (((expected_queue / openqueue)) * ((1 / mu) * 60 * 15)));
        }
    }

    /**
     * Esta función construye la matriz de estados de transición, necesaria en
     * en el
     *
     * @param size
     * @param time
     * @param store
     * @param oc
     * @param pc
     * @param dh
     * @return
     * @throws Exception
     */
    public Matrix RateTransition(int size, int time, String store, double oc, double pc[], double dh[]) throws Exception {
        double[][] a = new double[size][size];
        double new_lambda = lambda_properties(store, pc, (int) dh[0], (int) dh[1]);
        double self = selfcheckout((int) dh[0], store);
        double lambda = new_lambda * self;
        double mu = mu_properties((int) dh[0], store);
        Matrix A = new Matrix(a);
        for (int i = 0; i < A.getRowDimension(); i++) {
            for (int j = 0; j < A.getColumnDimension(); j++) {
                if (i == j - 1) {
                    A.set(i, j, lambda);
                }
                if (i == j + 1) {
                    A.set(i, j, oc * mu);
                }
            }
        }

        for (int i = 0; i < A.getRowDimension(); i++) {
            double diagonalvalue = 0.0;
            for (int j = 0; j < A.getColumnDimension(); j++) {
                if (j != i) {
                    diagonalvalue += A.get(i, j);
                }

            }
            A.set(i, i, -diagonalvalue);
        }


        return A;
    }

    /**
     * Este método busca en una matriz dada y retorna un double que corresponde
     * al negativo del valor mayor de la diagonal de la matriz examinada.
     *
     * @param B es una matriz de NxN
     * @return double max(-valor de la diagonal)
     */
    public double MaxRate(Matrix B) {
        double max = 0.0;
        for (int i = 0; i < B.getRowDimension(); i++) {
            for (int j = 0; j < B.getColumnDimension(); j++) {
                if (-B.get(i, j) > max) {
                    max = -B.get(i, j);
                }
            }
        }

        return max;

    }

    /**
     *
     * @param value
     * @return
     * @throws Exception
     */
    public double[] XML2Data(String value) throws Exception {

        DocumentBuilder db = DocumentBuilderFactory.newInstance().newDocumentBuilder();
        InputSource is = new InputSource();
        is.setCharacterStream(new StringReader(xml));
        Document doc = db.parse(is);

        if (value.equals("DH")) {
            double[] Data = new double[2];
            NodeList nodes = doc.getElementsByTagName("Proc");
            Element link = (Element) nodes.item(0);
            String dato = link.getAttribute("day");
            Data[1] = Double.parseDouble(dato);
            nodes = doc.getElementsByTagName("time");
            link = (Element) nodes.item(0);
            dato = link.getAttribute("time");
            int num = dato.indexOf(":");
            int ret = Integer.parseInt(dato.substring(0, num));
            Data[0] = ret;
            return Data;
        }
        if (value.equals("Q15_value")) {

            NodeList nodes = doc.getElementsByTagName("prediction");
            double[] Data = new double[nodes.getLength()];
            for (int i = 0; i < nodes.getLength(); i++) {
                Element link = (Element) nodes.item(i);
                Data[i] = Double.parseDouble(link.getAttribute("value"));
            }
            return Data;

        } else {
            NodeList nodes = doc.getElementsByTagName("variable");
            int counter = 0;
            for (int i = 0; i < nodes.getLength(); i++) {
                Element link = (Element) nodes.item(i);
                String dato = link.getAttribute("name");
                if (dato.equals(value)) {
                    counter++;
                }
            }
            double Data[] = new double[counter];
            int l = 0;
            for (int i = 0; i < nodes.getLength(); i++) {
                Element link = (Element) nodes.item(i);
                String dato = link.getAttribute("name");
                if (dato.equals(value)) {
                    Data[l] = Double.parseDouble(link.getAttribute("value"));
                    l++;
                }
            }
            return Data;

        }

    }

    /**
     * Esta función calcula la tasa de llegada de personas al sistema de colas
     * en base a las personas que ingresan al supermercado en los períodos
     * previos de observación.
     *
     * @param store es la tienda que se está evaluando.
     * @param pc es un arreglo con las llegadas de personas en los períodos.
     * @param day es el día de la semana (número) que
     * @param hour
     * @return
     * @throws Exception
     */
    public double lambda_properties(String store, double pc[], int day, int hour) throws Exception {
        double valor_lambda = 0.0;
        try {
            Properties propiedades = new Properties();
            propiedades.load(this.getClass().getClassLoader().getResourceAsStream("datos.properties"));
            CsvReader csv = new CsvReader(propiedades.getProperty("direccion"));
            double week_day[] = new double[2];
            double hour_day[] = new double[23];

            if (day == 2 || day == 3 || day == 4 || day == 5 || day == 6) {
                week_day[0] = 1.0;
            }

            if (day == 1 || day == 7) {
                week_day[1] = 1.0;
            }

            for (int i = 0; i < hour_day.length; i++) {
                if (i == hour && hour >= 7) {
                    hour_day[i] = 1.0;
                }
                if (hour < 7 || hour > 23) {
                    hour_day[i] = 1.0;
                }
            }
            csv.readRecord();
            int columns = 10;
            double[] theta = new double[columns];
            while (csv.readRecord()) {

                String store_id = (csv.get(0));
                if (store_id.equals(store)) {
                    valor_lambda = Double.parseDouble(csv.get(1));
                    for (int a = 2; a <= columns; a++) {
                        theta[a - 2] = Double.parseDouble(csv.get(a));
                    }
                    for (int i = 0; i < (int) Math.min(pc.length, theta.length); i++) {
                        valor_lambda += theta[i] * pc[i];
                    }

                    for (int b = 11; b < csv.getColumnCount(); b++) {
                        if (b == 11 || b == 12) {
                            valor_lambda += week_day[b - 11] * Double.parseDouble(csv.get(b));
                        }
                        if (b > 12 && b - 11 == hour) {
                            valor_lambda += hour_day[b - 11] * Double.parseDouble(csv.get(b));
                        }
                    }
                    break;
                }
            }
            csv.close();
        } catch (IOException e) {
            valor_lambda = -1.0;
        }

        return valor_lambda * selfcheckout(hour, store);

    }

    /**
     *
     * @param periodo
     * @param store
     * @return
     */
    public double mu_properties(int periodo, String store) {
        double mu = 0.0;
        try {
            Properties propiedades = new Properties();
            propiedades.load(this.getClass().getClassLoader().getResourceAsStream("datos.properties"));
            CsvReader csv = new CsvReader(propiedades.getProperty("direccion_mu"));
            csv.readRecord();
            while (csv.readRecord()) {
                String store_id = (csv.get(0));
                int hora = Integer.parseInt(csv.get(1));
                double mu_data = Double.parseDouble(csv.get(2));

                if (store_id.equals(store) && hora == periodo) {
                    mu = mu_data;
                }
            }
            csv.close();
        } catch (IOException e) {
            mu = -1.0;
        }

        return (1 / mu) * 900;
    }

    /**
     * @param periodo
     * @param store
     * @return
     */
    public double selfcheckout(int periodo, String store) {
        double percentage = 0.0;
        try {
            Properties propiedades = new Properties();
            propiedades.load(this.getClass().getClassLoader().getResourceAsStream("datos.properties"));
            CsvReader csv = new CsvReader(propiedades.getProperty("direccion_self"));
            csv.readRecord();
            while (csv.readRecord()) {
                String store_id = (csv.get(0));
                int hora = Integer.parseInt(csv.get(1));
                double self = Double.parseDouble(csv.get(2));

                if (store_id.equals(store) && hora == periodo) {
                    percentage = 1 - self;
                }
            }
            csv.close();
        } catch (IOException e) {
            percentage = -1.0;
        }

        return percentage;
    }

    /**
     * @param b
     * @param n
     * @return
     */
    public Matrix power(Matrix b, int n) {
        if (n == 0) {
            return identity(b.getColumnDimension()); // b to the 0 is 1
        }
        Matrix c = power(b, n / 2);
        c = c.times(c);
        if (n % 2 == 0) {
            return c; // case when n is even
        }
        return c.times(b); // case when n is odd
    }

    /**
     * @param N
     * @return
     */
    public Matrix identity(int N) {
        Matrix I = new Matrix(N, N);
        for (int i = 0; i < N; i++) {
            I.set(i, i, 1);
        }
        return I;
    }

    /**
     * @param a is the double's array resultant from queue model
     * @return a String XML
     */
    public String XmlOutputResult(double[] a) {
        String output = "<?xml version=\"1.0\" encoding=\"UTF-8\"?><Result>";
        for (int i = 0; i < a.length - 1; i++) {
            if (a[i] > maxcheckout) {
                a[i] = maxcheckout;
            }

            output += "<var name=\"Q" + 3 * (i + 5) + "\" value=\"" + a[i]
                    + "\" exql=\"" + a[i + 1] + "\" exwt=\"" + a[i + 2]
                    + "\" simulation=\"" + a[i + 3] + "\" peoplearrival=\"" + a[i + 4] + "\"/>";
            i = i + 4;
        }
        output += "</Result>";
        return output;
    }

    /**
     * @param Q
     * @param t
     * @return Matriz de probabilidades de transition
     */
    public final Matrix CTMCTransiente(Matrix Q, int t) {
        final double epsilon = 0.001;
        double lda = MaxRate(Q);
        double suma = 0.0;
        double oneminuseps = 1 - epsilon;
        double ldat = lda * t;
        double pk = Math.exp(-ldat);
        double fkp1 = 1 - suma;
        Integer n = Q.getRowDimension();
        Matrix ck = identity(n);
        Matrix p = (Q.times(1 / lda)).plus(ck);
        Integer k = 0;

        Matrix result = ck.times(pk);

        while (suma < oneminuseps) {
            k = k + 1;
            ck = ck.times(p);
            pk = pk * ldat / k;
            suma += pk;
            fkp1 = 1 - suma;
            result = result.plus(ck.times(pk));
        }

        return result;

    }

    /**
     * Esta función recupera información desde datos históricos del local en
     * ejecución para así tener el número de cajas y largo de cola cuando no es
     * posible calcular la información en tiempo real.
     *
     * @param store es el id de la tienda utilizada
     * @param hour es la hora a la que se esta realizando la ejecucion
     * @param day es el día de la semana que se ejecuta el algoritmo (0 para
     * Domingo y 7 para Sábado)
     * @return un arreglo de doubles con la cantidad de cajas en su primer
     * componente y el largo de cola en el segundo componente.
     */
    public double[] historical(String store, int hour, int day) {
        double[] result = new double[2];
        try {
            Properties propiedades = new Properties();
            propiedades.load(this.getClass().getClassLoader().getResourceAsStream("datos.properties"));
            CsvReader csv = new CsvReader(propiedades.getProperty("direccion_historical"));
            csv.readRecord();
            while (csv.readRecord()) {
                String store_id = (csv.get(0));
                int dia = Integer.parseInt(csv.get(1));
                int hora = Integer.parseInt(csv.get(2));
                int checkout = Integer.parseInt(csv.get(3));
                double queuelenght = Double.parseDouble(csv.get(4));
                if (store_id.equals(store) && hora == hour && dia == day) {
                    result[0] = checkout;
                    result[1] = queuelenght;
                }
            }
            csv.close();
        } catch (IOException e) {
            result[0] = -1.0;
            result[1] = -1.0;
        }
        return result;
    }
}